/*
  Copyright (C) 2001 Bertrik Sikken (bertrik@zonnet.nl)

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

  $Id: niash_core.c,v 1.8 2004-10-27 18:06:19 ullsig-guest Exp $
*/

/*
    Core NIASH chip functions.
*/

#include <stdio.h>		/* fopen, fread, fwrite, fclose etc */
#include <stdarg.h>		/* va_list for vfprintf */
#include <string.h>		/* memcpy, memset */
#include <unistd.h>		/* unlink */
#include <stdlib.h>		/* malloc, free */
#include <math.h>		/* exp, pow */

#include "niash_xfer.h"
#include "niash_core.h"


#ifndef MIN
#define MIN(a,b) (((a) < (b)) ? (a) : (b))
#endif

#ifndef MAX
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
#endif


#define XFER_BUF_SIZE  0xF000


/* HP3400 firmware data */
static unsigned char abData0000[] = {
	0xfe, 0x9f, 0x58, 0x1b, 0x00, 0x03, 0xa4, 0x02, 0x63, 0x02, 0x33,
	0x02,
	0x0d, 0x02, 0xf0, 0x01,
	0xd8, 0x01, 0xc5, 0x01, 0xb5, 0x01, 0xa8, 0x01, 0x9d, 0x01, 0x93,
	0x01,
	0x8b, 0x01, 0x84, 0x01,
	0x7e, 0x01, 0x79, 0x01, 0x74, 0x01, 0x70, 0x01, 0x6d, 0x01, 0x69,
	0x01,
	0x67, 0x01, 0x64, 0x01,
	0x62, 0x01, 0x60, 0x01, 0x5f, 0x01, 0x5d, 0x01, 0x5c, 0x01, 0x5b,
	0x01,
	0x5a, 0x01, 0x59, 0x01,
	0x58, 0x01, 0x57, 0x01, 0x57, 0x01, 0x56, 0x01, 0x56, 0x01, 0x55,
	0x01,
	0x55, 0x01, 0x54, 0x01,
	0x54, 0x01, 0x54, 0x01, 0x54, 0x01, 0x53, 0x01, 0x53, 0x01, 0x53,
	0x01,
	0x53, 0x01, 0x52, 0x81
};

/*  1st word : 0x9ffe = 40958, strip 15th bit: 0x1ffe = 8190
    2nd word : 0x1b58 = 7000 -> coincidence ?
    other words: formula: y = 676 / (2 - exp(0.113 * (1-x)) ), where x = 0 for first entry
*/

/* more HP3400 firmware data */
static unsigned char abData0400[] = {
	0xa4, 0x82, 0x00, 0x80, 0xa4, 0x82, 0xaa, 0x02, 0xc0, 0x02, 0xe8,
	0x02,
	0x3e, 0x03, 0xc8, 0x03,
	0x58, 0x1b, 0xfe, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00,
	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00
};



static void
_ConvertMotorTable(unsigned char *pabOld, unsigned char *pabNew, int iSize,
		   int iLpi)
{
	int iData, i, iBit15;

	for (i = 0; i < (iSize / 2); i++) {
		iData = pabOld[2 * i + 0] + (pabOld[2 * i + 1] << 8);
		iBit15 = (iData & 0x8000);
		iData = (iData & 0x7FFF);
		if (iData <= 0x400) {
			iData = iData * iLpi / 300;
		}
		if (iBit15 != 0) {
			iData |= 0x8000;
		}
		pabNew[2 * i + 0] = iData & 255;
		pabNew[2 * i + 1] = (iData >> 8) & 255;
	}
}


/*************************************************************************
  _ProbeRegisters
  ===============
    Tries to determine certain hardware properties.

  This is done by checking the writeability of some scanner registers.
  We cannot rely simply on the scanner model to contain a specific
  chip. The HP3300c for example uses one of at least three slightly
  different scanner ASICs (NIASH00012, NIASH00013 and NIASH00014).

  OUT pHWParams     Hardware parameters, updated fields:
        fGamma16    TRUE if 16 bit gamma tables can be used
        fReg07      TRUE if reg07 is writeable
        iBufferSize Size of scanner's internal buffer

  Returns TRUE if a NIASH chipset was found.
*************************************************************************/
static SANE_Bool
_ProbeRegisters(THWParams * pHWParams)
{
	unsigned char bData1, bData2;
	int iHandle;

	iHandle = pHWParams->iXferHandle;

	DBG(DBG_MSG, "Probing scanner...\n");

	/* check register 0x04 */
	NiashWriteReg(iHandle, 0x04, 0x55);
	NiashReadReg(iHandle, 0x04, &bData1);
	NiashWriteReg(iHandle, 0x04, 0xAA);
	NiashReadReg(iHandle, 0x04, &bData2);
	NiashWriteReg(iHandle, 0x04, 0x07);
	if ((bData1 != 0x55) || (bData2 != 0xAA)) {
		DBG(DBG_ERR, "  No NIASH chipset found!\n");
		return SANE_FALSE;
	}

	/* check writeability of register 3 bit 1 */
	NiashReadReg(iHandle, 0x03, &bData1);
	NiashWriteReg(iHandle, 0x03, bData1 | 0x02);
	NiashReadReg(iHandle, 0x03, &bData2);
	NiashWriteReg(iHandle, 0x03, bData1);
	pHWParams->fGamma16 = ((bData2 & 0x02) != 0);
	DBG(DBG_MSG, "  Gamma table entries are %d bit\n",
	    pHWParams->fGamma16 ? 16 : 8);

	/* check register 0x07 */
	NiashReadReg(iHandle, 0x07, &bData1);
	NiashWriteReg(iHandle, 0x07, 0x1C);
	NiashReadReg(iHandle, 0x07, &bData2);
	NiashWriteReg(iHandle, 0x07, bData1);
	pHWParams->fReg07 = (bData2 == 0x1C);

	if (!pHWParams->fGamma16) {
		/* internal scan buffer size is an educated guess, but seems to correlate
		   well with the size calculated from several windows driver log files
		   size = 128kB - 44088 unsigned chars (space required for gamma/calibration table)
		 */
		pHWParams->iBufferSize = 86984L;
		DBG(DBG_MSG, "  NIASH version < 00014\n");
	} else {
		pHWParams->iBufferSize = 0x60000L;
		if (!pHWParams->fReg07) {
			DBG(DBG_MSG, "  NIASH version = 00014\n");
		} else {
			DBG(DBG_MSG, "  NIASH version > 00014\n");
		}
	}

	return SANE_TRUE;
}


/* returns 0 on success, < 0 otherwise */
STATIC int
NiashOpen(THWParams * pHWParams, const char *pszName)
{
	int iXferHandle;

	iXferHandle = NiashXferOpen(pszName, &pHWParams->eModel);
	if (iXferHandle < 0) {
		DBG(DBG_ERR, "NiashXferOpen failed for '%s'\n", pszName);
		return -1;
	}

	pHWParams->iXferHandle = iXferHandle;

	NiashWakeup(pHWParams->iXferHandle);

	/* default HW params */
	pHWParams->iSensorSkew = 8;
	pHWParams->iTopLeftX = 0;
	pHWParams->iTopLeftY = 3;
	pHWParams->fReg07 = SANE_FALSE;
	pHWParams->iSkipLines = 0;
	pHWParams->iExpTime = 5408;
	pHWParams->iReversedHead = SANE_TRUE;

	switch (pHWParams->eModel) {

	case eHp3300c:
		DBG(DBG_MSG, "Setting params for Hp3300\n");
		pHWParams->iTopLeftX = 4;
		pHWParams->iTopLeftY = 11;
		pHWParams->iSkipLines = 14;
		break;

	case eHp3400c:
	case eHp4300c:
		DBG(DBG_MSG, "Setting params for Hp3400c/Hp4300c\n");
		pHWParams->iTopLeftX = 3;
		pHWParams->iTopLeftY = 14;
		pHWParams->fReg07 = SANE_TRUE;
		break;

	case eAgfaTouch:
		DBG(DBG_MSG, "Setting params for AgfaTouch\n");
		pHWParams->iReversedHead = SANE_FALSE;	/* head not reversed on Agfa Touch */
		pHWParams->iTopLeftX = 3;
		pHWParams->iTopLeftY = 10;
		pHWParams->iSkipLines = 7;
		break;

	case eUnknownModel:
		DBG(DBG_MSG, "Setting params for UnknownModel\n");
		break;

	default:
		DBG(DBG_ERR, "ERROR: internal error! (%d)\n",
		    (int) pHWParams->eModel);
		return -1;
	}

	/* autodetect some hardware properties */
	if (!_ProbeRegisters(pHWParams)) {
		DBG(DBG_ERR, "_ProbeRegisters failed!\n");
		return -1;
	}

	return 0;
}


STATIC void
NiashClose(THWParams * pHWPar)
{
	NiashXferClose(pHWPar->iXferHandle);
	pHWPar->iXferHandle = 0;
}


static void
WriteRegWord(int iHandle, unsigned char bReg, SANE_Word wData)
{
	NiashWriteReg(iHandle, bReg, wData & 0xFF);
	NiashWriteReg(iHandle, bReg + 1, (wData >> 8) & 0xFF);
}


/* calculate a 4096 unsigned char gamma table */
STATIC void
CalcGamma(unsigned char *pabTable, double Gamma)
{
	int i, iData;

	/* fill gamma table */
	for (i = 0; i < 4096; i++) {
		iData = floor(256.0 *
			      pow(((double) i / 4096.0), 1.0 / Gamma));
		pabTable[i] = iData;
	}
}


/*
  Hp3400WriteFw
  =============
    Writes data to scanners with a NIASH00019 chipset, e.g.
    gamma, calibration and motor control data.

  IN  pabData   pointer to firmware data
      iLen      Size of firmware date (unsigned chars)
      iAddr     Scanner address to write to
*/
static void
Hp3400cWriteFW(int iXferHandle, unsigned char *pabData, int iLen, int iAddr)
{
	iAddr--;
	NiashWriteReg(iXferHandle, 0x21, iAddr & 0xFF);
	NiashWriteReg(iXferHandle, 0x22, (iAddr >> 8) & 0xFF);
	NiashWriteReg(iXferHandle, 0x23, (iAddr >> 16) & 0xFF);
	NiashWriteBulk(iXferHandle, pabData, iLen);
}


/* Writes the gamma and offset/gain tables to the scanner.
   In case a calibration file exist, it will be used for offset/gain */
STATIC void
WriteGammaCalibTable(unsigned char *pabGammaR, unsigned char *pabGammaG,
		     unsigned char *pabGammaB, unsigned char *pabCalibTable,
		     int iGain, int iOffset, THWParams * pHWPar)
{
	int i, j, k;
	static unsigned char abGamma[60000];
	int iData;
	int iHandle;

	iHandle = pHWPar->iXferHandle;

	j = 0;
	/* fill gamma table for red component */
	/* pad entries with 0 for 16-bit gamma table */
	for (i = 0; i < 4096; i++) {
		if (pHWPar->fGamma16) {
			abGamma[j++] = 0;
		}
		abGamma[j++] = pabGammaR[i];
	}
	/* fill gamma table for green component */
	for (i = 0; i < 4096; i++) {
		if (pHWPar->fGamma16) {
			abGamma[j++] = 0;
		}
		abGamma[j++] = pabGammaG[i];
	}
	/* fill gamma table for blue component */
	for (i = 0; i < 4096; i++) {
		if (pHWPar->fGamma16) {
			abGamma[j++] = 0;
		}
		abGamma[j++] = pabGammaB[i];
	}

	if (pabCalibTable == NULL) {
		iData = (iGain << 6) + iOffset;
		for (i = 0; i < HW_PIXELS; i++) {
			for (k = 0; k < 3; k++) {
				abGamma[j++] = (iData) & 255;
				abGamma[j++] = (iData >> 8) & 255;
			}
		}
	} else {
		memcpy(&abGamma[j], pabCalibTable, HW_PIXELS * 6);
		j += HW_PIXELS * 6;
	}

	NiashWriteReg(iHandle, 0x02, 0x80);
	NiashWriteReg(iHandle, 0x03, 0x01);
	NiashWriteReg(iHandle, 0x03, 0x11);
	NiashWriteReg(iHandle, 0x02, 0x84);

	if (pHWPar->fReg07) {
		Hp3400cWriteFW(iHandle, abGamma, j, 0x2000);
	} else {
		NiashWriteBulk(iHandle, abGamma, j);
	}

	NiashWriteReg(iHandle, 0x02, 0x80);
}


static void
WriteAFEReg(int iHandle, int iReg, int iData)
{
	NiashWriteReg(iHandle, 0x25, iReg);
	NiashWriteReg(iHandle, 0x26, iData);
}


/* setup the analog front-end -> coarse calibration */
static void
WriteAFE(int iHandle)
{
	/* see WM8143 datasheet */

	WriteAFEReg(iHandle, 0x04, 0x00);
	WriteAFEReg(iHandle, 0x03, 0x12);
	WriteAFEReg(iHandle, 0x02, 0x04);
	WriteAFEReg(iHandle, 0x05, 0x10);
	WriteAFEReg(iHandle, 0x01, 0x03);

	WriteAFEReg(iHandle, 0x20, 0xc0);	/*c8 *//* red offset */
	WriteAFEReg(iHandle, 0x21, 0xc0);	/*c8 *//* green offset */
	WriteAFEReg(iHandle, 0x22, 0xc0);	/*d0 *//* blue offset */

	WriteAFEReg(iHandle, 0x28, 0x05);	/*5 *//* red gain */
	WriteAFEReg(iHandle, 0x29, 0x03);	/*3 *//* green gain */
	WriteAFEReg(iHandle, 0x2A, 0x04);	/*4 *//* blue gain */
}


/* wait for the carriage to return */
static void
WaitReadyBit(int iHandle)
{
	unsigned char bData;

	do {
		NiashReadReg(iHandle, 0x03, &bData);
	}
	while ((bData & 8) == 0);
}


/*
  Initialisation specific for NIASH00014 and lower chips
*/
static void
InitNiash00014(TScanParams * pParams, THWParams * pHWParams)
{
	int iHandle, iLpiCode;

	iHandle = pHWParams->iXferHandle;

	/* exposure time (in units 24/Fcrystal)? */
	WriteRegWord(iHandle, 0x08, pHWParams->iExpTime - 1);

	/* width in pixels */
	WriteRegWord(iHandle, 0x12, pParams->iWidth - 1);

	/* top */
	WriteRegWord(iHandle, 0x17, pParams->iTop);
	WriteRegWord(iHandle, 0x19, pParams->iTop);

	/* time between stepper motor steps (in units of 24/Fcrystal)? */
	iLpiCode = pParams->iLpi * pHWParams->iExpTime / 1200L;

	if (!pHWParams->fGamma16) {
		/* NIASH 00012 / 00013 init */

		/* LPI specific settings */
		if (pParams->iLpi < 600) {
			/* set halfres bit */
			NiashWriteReg(iHandle, 0x06, 0x01);
			/* double lpi code because of halfres bit */
			iLpiCode *= 2;
		} else {
			/* clear halfres bit */
			NiashWriteReg(iHandle, 0x06, 0x00);
			/* add exptime to make it scan slower */
			iLpiCode += pHWParams->iExpTime;
		}

		/* unknown setting */
		WriteRegWord(iHandle, 0x27, 0x7FD2);
		WriteRegWord(iHandle, 0x29, 0x6421);

	} else {
		/* NIASH 00014 init */

		/* halfres bit always cleared */
		NiashWriteReg(iHandle, 0x06, 0x00);

		/* LPI specific settings */
		if (pParams->iLpi >= 600) {
			/* add exptime to make it scan slower */
			iLpiCode += pHWParams->iExpTime;
		}

		/* unknown setting */
		WriteRegWord(iHandle, 0x27, 0xc862);	/*c862 */
		WriteRegWord(iHandle, 0x29, 0xb853);	/*b853 */
	}

	/* LPI code */
	WriteRegWord(iHandle, 0x0A, iLpiCode - 1);

	/* backtrack reversing speed */
	NiashWriteReg(iHandle, 0x1E, (iLpiCode - 1) / 32);
}


/*
  Initialisation specific for NIASH00019 chips
*/
static void
InitNiash00019(TScanParams * pParams, THWParams * pHWParams)
{
	int iHandle, iLpiCode;
	static unsigned char abMotor[512];


	iHandle = pHWParams->iXferHandle;

	/* exposure time (in units 24/Fcrystal)? */
	WriteRegWord(iHandle, 0x08, pHWParams->iExpTime);

	/* width in pixels */
	WriteRegWord(iHandle, 0x12, pParams->iWidth);

	/* ? */
	WriteRegWord(iHandle, 0x27, 0xc862);	/*c862 */
	WriteRegWord(iHandle, 0x29, 0xb853);	/*b853 */

	/* specific handling of 150 dpi resolution */
	if (pParams->iLpi == 150) {
		/* use 300 LPI but skip every other line */
		pParams->iLpi = 300;
		NiashWriteReg(iHandle, 0x06, 0x01);
	} else {
		NiashWriteReg(iHandle, 0x06, 0x00);
	}

	/* DPI and position table */
	NiashWriteReg(iHandle, 0x07, 0x02);
	_ConvertMotorTable(abData0000, abMotor, sizeof(abData0000),
			   pParams->iLpi);
	Hp3400cWriteFW(iHandle, abMotor, sizeof(abData0000), 0x000);
	_ConvertMotorTable(abData0400, abMotor, sizeof(abData0400),
			   pParams->iLpi);
	Hp3400cWriteFW(iHandle, abMotor, sizeof(abData0400), 0x400);

	/* backtrack reversing speed */
	iLpiCode = pParams->iLpi * pHWParams->iExpTime / 1200L;
	NiashWriteReg(iHandle, 0x1E, (iLpiCode - 1) / 32);
}


/*
  Scanner initialisation common to all NIASH chips
*/
static void
InitNiashCommon(TScanParams * pParams, THWParams * pHWParams)
{
	int iWidthHW, iHandle, iMaxLevel;


	iHandle = pHWParams->iXferHandle;

	NiashWriteReg(iHandle, 0x02, 0x80);
	NiashWriteReg(iHandle, 0x03, 0x11);
	NiashWriteReg(iHandle, 0x01, 0x8B);
	NiashWriteReg(iHandle, 0x05, 0x01);

	/* dpi */
	WriteRegWord(iHandle, 0x0C, pParams->iDpi);

	/* calculate width in units of HW resolution */
	iWidthHW = pParams->iWidth * (HW_DPI / pParams->iDpi);

	/* set left and right limits */
	if (pHWParams->iReversedHead) {
		/* head is reversed */
		/* right */
		WriteRegWord(iHandle, 0x0E,
			     3 * (HW_PIXELS - (pParams->iLeft + iWidthHW)));

		/* left */
		WriteRegWord(iHandle, 0x10,
			     3 * (HW_PIXELS - pParams->iLeft) - 1);
	} else {
		/* head is not reversed */
		/*left  */
		WriteRegWord(iHandle, 0x0E, 3 * pParams->iLeft);

		/* right */
		WriteRegWord(iHandle, 0x10,
			     3 * (pParams->iLeft + iWidthHW) - 1);
	}

	/* bottom */
	WriteRegWord(iHandle, 0x1B, pParams->iBottom);	/* 0x393C); */

	/* forward jogging speed */
	NiashWriteReg(iHandle, 0x1D, 0x60);

	/* backtrack reversing speed? */
	NiashWriteReg(iHandle, 0x2B, 0x15);

	/* backtrack distance */
	if (pParams->iLpi < 600) {
		NiashWriteReg(iHandle, 0x1F, 0x30);
	} else {
		NiashWriteReg(iHandle, 0x1F, 0x18);
	}

	/* max buffer level before backtrace */
	iMaxLevel = MIN(pHWParams->iBufferSize / pParams->iWidth, 250);
	NiashWriteReg(iHandle, 0x14, iMaxLevel - 1);

	/* lamp PWM, max = 0x1ff? */
	WriteRegWord(iHandle, 0x2C, 0x01FF);

	/* not needed? */
	NiashWriteReg(iHandle, 0x15, 0x90);	/* 90 */
	NiashWriteReg(iHandle, 0x16, 0x70);	/* 70 */

	WriteAFE(iHandle);

	WaitReadyBit(iHandle);

	NiashWriteReg(iHandle, 0x03, 0x05);

	NiashWriteReg(iHandle, 0x02, pParams->fCalib ? 0x88 : 0xA8);
}


/* write registers */
STATIC SANE_Bool
InitScan(TScanParams * pParams, THWParams * pHWParams)
{
	int iHeight;
	int iExpTime;
	TScanParams Params;
	int iHandle;

	iHandle = pHWParams->iXferHandle;

	/* check validity of scanparameters */
	switch (pParams->iDpi) {
	case 150:
	case 300:
	case 600:
		break;
	default:
		DBG(DBG_ERR, "Invalid dpi (%d)\n", pParams->iDpi);
		return SANE_FALSE;
	}

	iHeight = (pParams->iBottom - pParams->iTop + 1);
	if (iHeight <= 0) {
		DBG(DBG_ERR, "Invalid height (%d)\n", iHeight);
		return SANE_FALSE;
	}

	if (pParams->iWidth <= 0) {
		DBG(DBG_ERR, "Invalid width (%d)\n", pParams->iWidth);
		return SANE_FALSE;
	}

	switch (pParams->iLpi) {
	case 150:
	case 300:
	case 600:
		break;
	default:
		DBG(DBG_ERR, "Invalid lpi (%d)\n", pParams->iLpi);
		return SANE_FALSE;
	}

	/* exposure time (in units of 24/Fcrystal?), must be divisible by 8 !!! */
	iExpTime = 5408;
	if ((iExpTime % 8) != 0) {
		DBG(DBG_ERR, "Invalid exposure time (%d)\n", iExpTime);
		return SANE_FALSE;
	}

	/*
	 *** Done checking scan parameters validity ***
	 */

	/*
	   copy the parameters locally and make pParams point to the local copy
	 */
	memcpy(&Params, pParams, sizeof(Params));
	pParams = &Params;

	if (!pHWParams->fReg07) {
		/* init NIASH00014 and lower */
		InitNiash00014(pParams, pHWParams);
	} else {
		/* init NIASH00019 */
		InitNiash00019(pParams, pHWParams);
	}

	/* common NIASH init */
	InitNiashCommon(pParams, pHWParams);

	return SANE_TRUE;
}


/************************************************************************/

static SANE_Bool
XferBufferGetLine(int iHandle, TDataPipe * p, unsigned char *pabLine,
		  SANE_Bool fReturn)
{
	unsigned char bData, bData2;
	SANE_Bool fJustDone = SANE_FALSE;
	/* all calculated transfers done ? */
	if (p->iLinesLeft == 0)
		return SANE_FALSE;

	/* time for a fresh read? */
	if (p->iCurLine == 0) {
		int iLines;
		iLines = p->iLinesPerXferBuf;
		/* read only as many lines as needed */
		if (p->iLinesLeft > 0 && p->iLinesLeft <= iLines) {
			iLines = p->iLinesLeft;
			DBG(DBG_MSG, "\n");
			DBG(DBG_MSG, "last bulk read\n");
			if (iLines < p->iLinesPerXferBuf) {
				DBG(DBG_MSG,
				    "reading reduced number of lines: %d instead of %d\n",
				    iLines, p->iLinesPerXferBuf);
			}
			fJustDone = SANE_TRUE;
		}
		/* reading old buffer level */
		NiashReadReg(iHandle, 0x20, &bData);
		NiashReadBulk(iHandle, p->pabXferBuf,
			      iLines * p->iBytesPerLine);
		/* reding new buffer level */
		NiashReadReg(iHandle, 0x20, &bData2);
		if (fJustDone && fReturn) {
			NiashWriteReg(iHandle, 0x02, 0x80);
			DBG(DBG_MSG, "returning scanner head\n");
		}
		DBG(DBG_MSG,
		    "buffer level = %3d, <reading %5d unsigned chars>, buffer level = %3d\r",
		    (int) bData, iLines * p->iBytesPerLine, (int) bData2);
		fflush(stdout);
	}
	/* copy one line */
	if (pabLine != NULL) {
		memcpy(pabLine,
		       &p->pabXferBuf[p->iCurLine * p->iBytesPerLine],
		       p->iBytesPerLine);
	}
	/* advance pointer */
	p->iCurLine = (p->iCurLine + 1) % p->iLinesPerXferBuf;

	/* one transfer line less to the XFerBuffer */
	if (p->iLinesLeft > 0)
		--(p->iLinesLeft);
	return SANE_TRUE;
}


static void
XferBufferInit(int iHandle, TDataPipe * p)
{
	int i;

	p->pabXferBuf = (unsigned char *) malloc(XFER_BUF_SIZE);
	p->iCurLine = 0;

	/* skip garbage lines */
	for (i = 0; i < p->iSkipLines; i++) {
		XferBufferGetLine(iHandle, p, NULL, SANE_FALSE);
	}
}

/* static procedure that fills the circular buffer in advance to any
   circular buffer data retrieval */
static void
CircBufferFill(int iHandle, TDataPipe * p, SANE_Bool iReversedHead)
{
	int i;
	for (i = 0; i < p->iLinesPerCircBuf; i++) {
		if (iReversedHead) {
			XferBufferGetLine(iHandle, p,
					  &p->pabCircBuf[p->iRedLine *
							 p->iBytesPerLine],
					  SANE_FALSE);
		} else {
			XferBufferGetLine(iHandle, p,
					  &p->pabCircBuf[p->iBluLine *
							 p->iBytesPerLine],
					  SANE_FALSE);
		}
		/* advance pointers */
		p->iRedLine = (p->iRedLine + 1) % p->iLinesPerCircBuf;
		p->iGrnLine = (p->iGrnLine + 1) % p->iLinesPerCircBuf;
		p->iBluLine = (p->iBluLine + 1) % p->iLinesPerCircBuf;
	}
}

static void
XferBufferExit(TDataPipe * p)
{
	if (p->pabXferBuf != NULL) {
		free(p->pabXferBuf);
		p->pabXferBuf = NULL;
	} else {
		DBG(DBG_ERR, "XferBufExit: Xfer buffer not initialised!\n");
	}
}


/* unscrambles a line:
   - combining the proper R, G and B lines and converting them to interpixel RGB
   - mirroring left to right
*/
static void
_UnscrambleLine(unsigned char *pabLine,
		unsigned char *pabRed, unsigned char *pabGrn,
		unsigned char *pabBlu, int iWidth, SANE_Bool iReversedHead,
		int iScaleDownDpi, int iBufWeight)
{
	/* never change an approved algorithm ...
	   so take Bertriks original source for this special case */
	if (iScaleDownDpi == 1 && iBufWeight == 0) {
		int i, j;
		if (iReversedHead) {
			/* reversed */
			for (i = 0; i < iWidth; i++) {
				j = (iWidth - i) * 3;
				pabLine[j - 3] = pabRed[i];
				pabLine[j - 2] = pabGrn[i + iWidth];
				pabLine[j - 1] = pabBlu[i + iWidth * 2];
			}
		} else {
			/* not reversed */
			for (i = 0; i < iWidth; i++) {
				pabLine[3 * i] = pabRed[i];
				pabLine[3 * i + 1] = pabGrn[i + iWidth];
				pabLine[3 * i + 2] = pabBlu[i + iWidth * 2];
			}
		}
	} else {
		int i, j;	/* loop variables */
		int c;		/* color buffer accumulator for horizontal avarage */

		/* initialize for incremental color buffer access */
		int iInc = 1;
		int iStart = 0;

		/* set for "from the end to the front" of the circular color buffers */
		if (iReversedHead) {
			iStart = iWidth - iScaleDownDpi;
			iInc = -1;
		}

		/* each pixel is the mean of iScaleDownDpi
		   so set the skip width accordingly */
		iInc *= iScaleDownDpi;

		for (i = iStart; i >= 0 && i < iWidth; i += iInc) {
			/* collect the red pixels */
			for (c = j = 0; j < iScaleDownDpi; ++j)
				c += pabRed[i + j];
			*pabLine =
				(*pabLine * iBufWeight +
				 c / iScaleDownDpi) / (iBufWeight + 1);
			pabLine++;

			/* collect the green pixels */
			for (c = j = 0; j < iScaleDownDpi; ++j)
				c += pabGrn[i + iWidth + j];
			*pabLine =
				(*pabLine * iBufWeight +
				 c / iScaleDownDpi) / (iBufWeight + 1);
			pabLine++;

			/* collect the blue pixels */
			for (c = j = 0; j < iScaleDownDpi; ++j)
				c += pabBlu[i + 2 * iWidth + j];
			*pabLine =
				(*pabLine * iBufWeight +
				 c / iScaleDownDpi) / (iBufWeight + 1);
			pabLine++;
		}
	}

}


/* gets an unscrambled line from the circular buffer. the first couple of lines contain garbage,
   if fReturn==SANE_TRUE, the head will return automatically on an end of scan */
STATIC SANE_Bool
CircBufferGetLineEx(int iHandle, TDataPipe * p, unsigned char *pabLine,
		    SANE_Bool iReversedHead, SANE_Bool fReturn)
{
	int iLineCount;
	for (iLineCount = 0; iLineCount < p->iScaleDownLpi; ++iLineCount) {
		if (iReversedHead) {
			if (!XferBufferGetLine(iHandle, p,
					       &p->pabCircBuf[p->iRedLine *
							      p->
							      iBytesPerLine],
					       fReturn))
				return SANE_FALSE;
		} else {
			if (!XferBufferGetLine(iHandle, p,
					       &p->pabCircBuf[p->iBluLine *
							      p->
							      iBytesPerLine],
					       fReturn))
				return SANE_FALSE;
		}
		if (pabLine != NULL) {
			_UnscrambleLine(pabLine,
					&p->pabCircBuf[p->iRedLine *
						       p->iBytesPerLine],
					&p->pabCircBuf[p->iGrnLine *
						       p->iBytesPerLine],
					&p->pabCircBuf[p->iBluLine *
						       p->iBytesPerLine],
					p->iWidth * p->iScaleDownDpi,
					iReversedHead, p->iScaleDownDpi,
					iLineCount);
		}

		/* advance pointers */
		p->iRedLine = (p->iRedLine + 1) % p->iLinesPerCircBuf;
		p->iGrnLine = (p->iGrnLine + 1) % p->iLinesPerCircBuf;
		p->iBluLine = (p->iBluLine + 1) % p->iLinesPerCircBuf;
	}
	return SANE_TRUE;
}


/* gets an unscrambled line from the circular buffer. the first couple of lines contain garbage */
STATIC SANE_Bool
CircBufferGetLine(int iHandle, TDataPipe * p, unsigned char *pabLine,
		  SANE_Bool iReversedHead)
{
	return CircBufferGetLineEx(iHandle, p, pabLine, iReversedHead,
				   SANE_FALSE);
}


/* try to keep the number of transfers the same, but make them all 
   as good as possible the same size to avoid cranking in critical
   situations
*/
static int
_OptimizeXferSize(int iLines, int iLinesPerXfer)
{
	int iXfers;
	iXfers = (iLines + iLinesPerXfer - 1) / iLinesPerXfer;
	while (--iLinesPerXfer > 0
	       && (iLines + iLinesPerXfer - 1) / iLinesPerXfer == iXfers);
	return iLinesPerXfer + 1;
}

STATIC void
CircBufferInit(int iHandle, TDataPipe * p,
	       int iWidth, int iHeight,
	       int iMisAlignment, SANE_Bool iReversedHead,
	       int iScaleDownDpi, int iScaleDownLpi)
{

	/* relevant for internal read and write functions */
	p->iScaleDownLpi = iScaleDownLpi;
	p->iScaleDownDpi = iScaleDownDpi;
	p->iWidth = iWidth;
	p->iBytesPerLine = iWidth * iScaleDownDpi * BYTES_PER_PIXEL;
	p->iSaneBytesPerLine = iWidth * BYTES_PER_PIXEL;
	if (iMisAlignment == 0) {
		p->iLinesPerCircBuf = 1;
	} else {
		p->iLinesPerCircBuf = 3 * iMisAlignment;
	}

	DBG(DBG_MSG, "_iScaleDown (Dpi,Lpi) = (%d,%d)\n", p->iScaleDownDpi,
	    p->iScaleDownLpi);
	DBG(DBG_MSG, "_iBytesPerLine = %d\n", p->iBytesPerLine);
	DBG(DBG_MSG, "_iLinesPerCircBuf = %d\n", p->iLinesPerCircBuf);
	p->pabCircBuf =
		(unsigned char *) malloc(p->iBytesPerLine *
					 p->iLinesPerCircBuf);
	if (p->pabCircBuf == NULL) {
		DBG(DBG_ERR,
		    "Unable to allocate %d unsigned chars for circular buffer\n",
		    (int) (p->iBytesPerLine * p->iLinesPerCircBuf));
		return;
	}
	DBG(DBG_MSG, "Allocated %d unsigned chars for circular buffer\n",
	    p->iBytesPerLine * p->iLinesPerCircBuf);

	if (iReversedHead) {
		p->iBluLine = 0;
		p->iGrnLine = iMisAlignment;
		p->iRedLine = iMisAlignment * 2;
	} else {
		p->iRedLine = 0;
		p->iGrnLine = iMisAlignment;
		p->iBluLine = iMisAlignment * 2;
	}

	/* negative height is an indication for "no Check" */
	if (iHeight < 0) {
		p->iLinesLeft = -1;
		p->iLinesPerXferBuf = XFER_BUF_SIZE / p->iBytesPerLine;
		DBG(DBG_MSG, "using unchecked XFER_BUF_SIZE\n");
		DBG(DBG_MSG, "_iXFerSize = %d\n",
		    p->iBytesPerLine * p->iLinesPerXferBuf);
	} else {
#define SAFETY_LINES 0
#define MAX_LINES_PER_XFERBUF 800
		/* estimate of number of unsigned chars to transfer at all via the USB */
		/* add some lines for securtiy */

		p->iLinesLeft =
			iHeight + p->iSkipLines + p->iLinesPerCircBuf +
			SAFETY_LINES;
		p->iLinesPerXferBuf = XFER_BUF_SIZE / p->iBytesPerLine;
		/* with more than 800 lines the timing is spoiled */
		if (p->iLinesPerXferBuf > MAX_LINES_PER_XFERBUF) {
			p->iLinesPerXferBuf = MAX_LINES_PER_XFERBUF;
		}
		/* final optimization to keep critical scans smooth */
		p->iLinesPerXferBuf =
			_OptimizeXferSize(p->iLinesLeft, p->iLinesPerXferBuf);

		DBG(DBG_MSG, "_iXFerSize = %d for %d transfer(s)\n",
		    (int) p->iLinesPerXferBuf * p->iBytesPerLine,
		    (p->iLinesLeft + p->iLinesPerXferBuf -
		     1) / p->iLinesPerXferBuf);
	}
	DBG(DBG_MSG, "_iLinesPerXferBuf = %d\n", p->iLinesPerXferBuf);

	/* init transfer buffer */
	XferBufferInit(iHandle, p);

	/* fill circular buffer */
	CircBufferFill(iHandle, p, iReversedHead);
}


STATIC void
CircBufferExit(TDataPipe * p)
{
	XferBufferExit(p);
	if (p->pabCircBuf != NULL) {
		DBG(DBG_MSG, "\n");
		free(p->pabCircBuf);
		p->pabCircBuf = NULL;
	} else {
		DBG(DBG_ERR,
		    "CircBufferExit: Circular buffer not initialised!\n");
	}
}


/************************************************************************/



static int
_CalcAvg(unsigned char *pabBuf, int n, int iStep)
{
	int i, j, x;

	for (i = j = x = 0; i < n; i++) {
		x += pabBuf[j];
		j += iStep;
	}
	return (x / n);
}


/* converts white line data and black point data into a calibration table */
static void
CreateCalibTable(unsigned char *abWhite, unsigned char bBlackR,
		 unsigned char bBlackG, unsigned char bBlackB,
		 int iReversedHead, unsigned char *pabCalibTable)
{
	int i, j, iGain, iOffset, iData;
	unsigned char *pabPixel;

	j = 0;
	for (i = 0; i < HW_PIXELS; i++) {
		if (iReversedHead) {
			pabPixel = &abWhite[(HW_PIXELS - i - 1) * 3];
		} else {
			pabPixel = &abWhite[i * 3];
		}
		/* red */
		if (bBlackR > 16)
			bBlackR = 16;
		iGain = 65536 / MAX(1, pabPixel[0] - bBlackR);
		iOffset = bBlackR * 4;
		if (iOffset > 63)
			iOffset = 63;
		iData = (iGain << 6) + iOffset;
		pabCalibTable[j++] = (iData) & 255;
		pabCalibTable[j++] = (iData >> 8) & 255;
		/* green */
		if (bBlackG > 16)
			bBlackG = 16;
		iGain = 65536 / MAX(1, pabPixel[1] - bBlackG);
		iOffset = bBlackG * 4;
		if (iOffset > 63)
			iOffset = 63;
		iData = (iGain << 6) + iOffset;
		pabCalibTable[j++] = (iData) & 255;
		pabCalibTable[j++] = (iData >> 8) & 255;
		/* blue */
		if (bBlackB > 16)
			bBlackB = 16;
		iGain = 65536 / MAX(1, pabPixel[2] - bBlackB);
		iOffset = bBlackB * 4;
		if (iOffset > 63)
			iOffset = 63;
		iData = (iGain << 6) + iOffset;
		pabCalibTable[j++] = (iData) & 255;
		pabCalibTable[j++] = (iData >> 8) & 255;
	}
}


/*************************************************************************
  Lamp control functions
*************************************************************************/
STATIC SANE_Bool
GetLamp(THWParams * pHWParams, SANE_Bool * pfLampIsOn)
{
	unsigned char bData;

	NiashReadReg(pHWParams->iXferHandle, 0x03, &bData);
	*pfLampIsOn = ((bData & 0x01) != 0);
	return SANE_TRUE;
}


STATIC SANE_Bool
SetLamp(THWParams * pHWParams, SANE_Bool fLampOn)
{
	unsigned char bData;
	int iHandle;

	iHandle = pHWParams->iXferHandle;

	NiashReadReg(iHandle, 0x03, &bData);
	if (fLampOn) {
		NiashWriteReg(iHandle, 0x03, bData | 0x01);
	} else {
		NiashWriteReg(iHandle, 0x03, bData & ~0x01);
	}
	return SANE_TRUE;
}


/*************************************************************************
  Experimental simple calibration, but also returning the white levels
*************************************************************************/
STATIC SANE_Bool
SimpleCalibExt(THWParams * pHWPar, unsigned char *pabCalibTable,
	       unsigned char *pabCalWhite)
{
	unsigned char bMinR, bMinG, bMinB;
	TDataPipe DataPipe;
	TScanParams Params;
	unsigned char abGamma[4096];
	int i, j;
	static unsigned char abBuf[HW_PIXELS * 3 * 71];	/* Carefull : see startWhite and endWhite below */
	static unsigned char abLine[HW_PIXELS * 3];
	static unsigned char abWhite[HW_PIXELS * 3];
	unsigned char *pabWhite;
	int iWhiteR, iWhiteG, iWhiteB;
	int iHandle;
	SANE_Bool iReversedHead;
	int startWhiteY, endWhiteY;
	int startBlackY, endBlackY;
	int startBlackX, endBlackX;

	iHandle = pHWPar->iXferHandle;
	iReversedHead = pHWPar->iReversedHead;

	DataPipe.iSkipLines = pHWPar->iSkipLines;

	Params.iDpi = HW_DPI;
	Params.iLpi = HW_DPI;
	if (iReversedHead)	/* hp scanners */
		Params.iTop = 60;
	else			/* agfa scanners */
		Params.iTop = 30;
	Params.iBottom = HP3300C_BOTTOM;
	Params.iLeft = 0;
	Params.iWidth = HW_PIXELS;
	Params.iHeight = 54;
	Params.fCalib = SANE_TRUE;

	/* write gamma table with neutral gain / offset */
	CalcGamma(abGamma, 1.0);
	WriteGammaCalibTable(abGamma, abGamma, abGamma, NULL, 256, 0, pHWPar);

	if (!InitScan(&Params, pHWPar)) {
		if (pabCalWhite)
			pabCalWhite[0] = pabCalWhite[1] = pabCalWhite[2] = 0;
		return SANE_FALSE;
	}

	/* Definition of white and black areas */
	if (iReversedHead) {	/* hp scanners */
		startWhiteY = 0;
		endWhiteY = 15;
		startBlackY = 16;
		endBlackY = 135;
		startBlackX = 0;
		endBlackX = HW_PIXELS;
	} else {		/* agfa scanners */
		startWhiteY = 0;
		endWhiteY = 70;
		startBlackY = 86;
		endBlackY = 135;
		startBlackX = 1666;
		endBlackX = 3374;
	}

	CircBufferInit(iHandle, &DataPipe, HW_PIXELS, -1, Params.iLpi / 150,
		       iReversedHead, 1, 1);
	/* white level */
	/* skip some lines */
	for (i = 0; i < startWhiteY; i++) {
		CircBufferGetLine(iHandle, &DataPipe, abLine, iReversedHead);
	}
	/* Get white lines */
	for (i = 0; i < endWhiteY - startWhiteY + 1; i++) {
		CircBufferGetLine(iHandle, &DataPipe,
				  &abBuf[i * HW_PIXELS * 3], iReversedHead);
	}
	/* black level */
	bMinR = 255;
	bMinG = 255;
	bMinB = 255;
	/* Skip some lines */
	for (i = 0; i < startBlackY; i++) {
		CircBufferGetLine(iHandle, &DataPipe, abLine, iReversedHead);
	}
	for (i = 0; i < endBlackY - startBlackY + 1; i++) {
		CircBufferGetLine(iHandle, &DataPipe, abLine, iReversedHead);
		for (j = 0; j < endBlackX; j++) {
			bMinR = MIN(abLine[j * 3 + 0], bMinR);
			bMinG = MIN(abLine[j * 3 + 1], bMinG);
			bMinB = MIN(abLine[j * 3 + 2], bMinB);
		}
	}
	CircBufferExit(&DataPipe);
	FinishScan(pHWPar);

	/* calc average white level */
	pabWhite = abBuf;
	for (i = 0; i < HW_PIXELS; i++) {
		abWhite[i * 3 + 0] =
			_CalcAvg(&pabWhite[i * 3 + 0],
				 endWhiteY - startWhiteY + 1, HW_PIXELS * 3);
		abWhite[i * 3 + 1] =
			_CalcAvg(&pabWhite[i * 3 + 1],
				 endWhiteY - startWhiteY + 1, HW_PIXELS * 3);
		abWhite[i * 3 + 2] =
			_CalcAvg(&pabWhite[i * 3 + 2],
				 endWhiteY - startWhiteY + 1, HW_PIXELS * 3);
	}
	iWhiteR = _CalcAvg(&abWhite[0], HW_PIXELS, 3);
	iWhiteG = _CalcAvg(&abWhite[1], HW_PIXELS, 3);
	iWhiteB = _CalcAvg(&abWhite[2], HW_PIXELS, 3);

	DBG(DBG_MSG, "Black level (%d,%d,%d), White level (%d,%d,%d)\n",
	    (int) bMinR, (int) bMinG, (int) bMinB, iWhiteR, iWhiteG, iWhiteB);

	/* convert the white line and black point into a calibration table */
	CreateCalibTable(abWhite, bMinR, bMinG, bMinB, iReversedHead,
			 pabCalibTable);
	/* assign the White Levels */
	if (pabCalWhite) {
		pabCalWhite[0] = iWhiteR;
		pabCalWhite[1] = iWhiteG;
		pabCalWhite[2] = iWhiteB;
	}
	return SANE_TRUE;
}


/*************************************************************************
  FinishScan
  ==========
    Finishes the scan. Makes the scanner head move back to the home position.

*************************************************************************/
STATIC void
FinishScan(THWParams * pHWParams)
{
	NiashWriteReg(pHWParams->iXferHandle, 0x02, 0x80);
}
